feat(email): extract action items into a persistent task list#1917
Conversation
Triage returned action_items inline-only, so commitments extracted from email evaporated with the response. The REST triage endpoints now also write each extracted item to a persistent email_tasks table (the same SQLite as the action log), linked back to the source message_id and de-duplicated per message on the normalized description — re-triaging a message never duplicates tasks. Additive: the wire response, contract, and SCHEMA_VERSION are unchanged; results without a message_id are not persisted. The cross-agent L8 task store (#1521) is not in-tree yet, so the store is email-local: task_store.record_action_items is the single seam that becomes the adapter forwarding tasks (source_ref=message_id) when it lands. MCP stdio triage stays analyze-only for now — persistence is wired at the REST surface the Agent UI drives.
|
Verdict: Approve with suggestions — one edge-path reliability issue worth fixing before merge. This PR persists triage action items into a local The one thing to address: the new persistence runs after the triage result is computed but its errors aren't isolated. If a task write fails, a successful triage now returns a 500 instead of the result — and for the batch endpoint a single item's write failure collapses the entire per-item-isolated response into one 500, breaking the documented "per-item failures isolate" contract. The most concrete trigger is two overlapping triages of the same message racing on the 🔍 Technical details🟡 ImportantPersistence errors aren't isolated from the triage response — breaks batch per-item isolation (
The most realistic trigger is a race, not disk failure: Cleanest targeted fix — treat the That handles the race for both endpoints. Separately, the batch loop should keep a single item's persistence failure from sinking the whole response — the batch already isolates per-item triage failures, and the side-effect should match that (e.g. persist per-item under a narrow guard that logs and continues, rather than letting it propagate past 🟢 Minor
Strengths
|
# Conflicts: # docs/guides/email.mdx # hub/agents/npm/agent-email/CHANGELOG.md
A duplicate-message race in record_action_items (two concurrent triages of the same message both pass the pre-insert dedup check, then race the UNIQUE index) raised sqlite3.IntegrityError uncaught, turning a successful triage into an unhandled 500 — and for /triage/batch it collapsed the documented per-item-isolated response into one failure. task_store.record_action_items now catches IntegrityError from that specific race and treats it as the dedup invariant firing (skip); any other exception still propagates. api_routes.py's persistence call sites (_triage_and_persist, _triage_batch_and_persist) catch and log any remaining persistence failure per item instead of letting it propagate, so a side-effect failure never flips an already-successful triage result into an error.
|
Branch updated to main, the review point is fixed, and the feature is verified working end-to-end on real hardware — this PR is ready to merge. Review point addressed (4b0c9a7): Real-world proof — Ryzen AI box (Radeon, Ubuntu), Lemonade v10.8.1 + Gemma-4-E4B, live inference, no OAuth:
Notes
|
# Conflicts: # hub/agents/npm/agent-email/CHANGELOG.md
The "API Tests" check has been red on every branch including main since
the email agent moved to a standalone hub wheel: the workflow installs
the routing and code hub packages into its venv but not
`gaia-agent-email`, so the email REST router never mounts and every
`/v1/email` test in `tests/test_api.py` fails with `404 {"detail":"Not
Found"}`. One install line fixes it.
**Test plan**
- [ ] "API Tests" check on this PR goes green (the email tests were the
only failures — same signature as main run 28462826567)
- [ ] After merge, re-run API Tests on open PRs amd#1917/amd#1919 — they
should go fully green
<details>
<summary>Evidence</summary>
- Main's latest run of this workflow (28462826567 @ 50c17de) fails with
the identical email-route 404 list as every PR branch — e.g.
`TestEmailTriageEndpoint::test_single_email_in_structured_out`: `assert
404 == 200`.
- Last green main run of this workflow was amd#1455 (2026-06-26) — the last
commit to touch `test_api.yml`; the failures start with the
hub-migration commits that followed.
- The workflow already installs the other two hub wheels for exactly
this reason (routing/code, amd#1102) — email was missed.
</details>
Co-authored-by: Tomasz Iniewicz <tomasz@iniewicz.com>
Action items extracted during triage were inline-only — useful in the moment, gone with the response, so commitments buried across threads silently slipped. Now every
POST /v1/email/triage/triage/batchalso persists each extracted item as a task row in the sidecar's local SQLite, linked back to the sourcemessage_idand de-duplicated per message, so re-triaging never duplicates a task. Fully additive: the wire response, contract, andSCHEMA_VERSIONare unchanged.The L8 cross-agent task store (#1521) isn't in-tree yet, so tasks persist to an email-local
email_taskstable;task_store.record_action_itemsis the single seam that becomes the adapter forwarding tasks (source_ref=message_id) when that EPIC lands. MCP stdio triage stays analyze-only for now — persistence is wired at the REST surface the Agent UI drives (wiring the MCP subprocess needs a test seam likeGAIA_EMAIL_MCP_FAKE_SEND; noted as follow-up).Closes #1605
Test plan
python -m pytest hub/agents/python/email/tests/test_email_task_store.py -v— store unit tests (create/link, no-dup on re-record, normalized dedup key, fail-loud on missing message_id) + REST surface tests (triage persists linked tasks, re-triage creates no duplicates, batch dedups per message)python -m pytest hub/agents/python/email/tests/ tests/test_email_openapi_conformance.py tests/unit/agents/email/ tests/mcp/test_email_mcp_stdio_parity.py tests/test_api.py— 733 passed (contract freshness, MCP parity, existing triage behavior unchanged)python util/lint.py --all— clean